/*
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
 * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
 * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
 * IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR
 * OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
 * ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
 * OTHER DEALINGS IN THE SOFTWARE.
 */
package com.packtpub.learningcypher.chapter5.cypher;

import com.packtpub.learningcypher.chapter5.model.*;
import java.util.*;
import org.neo4j.cypher.javacompat.*;
import org.neo4j.graphdb.*;
import org.neo4j.helpers.collection.IteratorUtil;

/**
 * Implementation of a {@code ReferenceRepository} using Cypher.
 *
 * @author Onofrio Panzarino
 */
public class CypherReferenceRepository implements ReferenceRepository, ReferenceStats, AutoCloseable {

    final ExecutionEngine engine;
    final GraphDatabaseService graphDb;

    public CypherReferenceRepository(ExecutionEngine engine, GraphDatabaseService graphDb) {
        this.engine = engine;
        this.graphDb = graphDb;
    }

    @Override
    public Reference byId(int id) {
        throw new UnsupportedOperationException("Not supported yet."); //To change body of generated methods, choose Tools | Templates.
    }

    @Override
    public Set<Reference> findByTitle(String title) {
        final String query = "MATCH (a:Reference) WHERE a.title =~ {title} RETURN a";
        Map<String, Object> params = new HashMap<>();
        params.put("title", ".*" + title + ".*");
        ExecutionResult result = engine.execute(query, params);

        final Set<Reference> ret = new HashSet<>();
        ResourceIterator<Node> n = result.columnAs("a");
        for (Node node : IteratorUtil.asIterable(n)) {
            ret.add(loadFrom(node));
        }
        return ret;
    }

    @Override
    public void save(Reference reference) {
        final String query = "CREATE (:Reference { id: {id}, title: {title}, abstractText: {abstractText}})";

        final Map<String, Object> params = new HashMap<>();
        params.put("id", reference.getId());
        params.put("title", reference.getTitle());
        params.put("abstractText", reference.getAbstractText());

        engine.execute(query, params);
    }

    @Override
    public void save(Author author) {
        final String query;
        final Map<String, Object> params = new HashMap<>();
        params.put("name", author.getName());
        params.put("surname", author.getSurname());

        if (author.getId() > 0) {
            query = "CREATE (:Author { id: {id}, name: {name}, surname: {surname} })";
            params.put("id", author.getId());
        } else {
            query = "MATCH (a:Author) WITH MAX(a.id) as lastId CREATE (:Author { id: COALESCE(lastId, 0)+1, name: {name}, surname: {surname} }) RETURN id";
        }
        engine.execute(query, params);
    }

    @Override
    public List<Author> getAuthors(Reference entry) {
        throw new UnsupportedOperationException("Not supported yet."); //To change body of generated methods, choose Tools | Templates.
    }

    @Override
    public Set<Reference> getCitedReferences(Reference entry) {
        throw new UnsupportedOperationException("Not supported yet."); //To change body of generated methods, choose Tools | Templates.
    }

    @Override
    public void setTags(Reference reference, Set<String> tags) {
        final String query = "MATCH (r:Reference{id: {id}}) SET r.tags = {tags}";
        final Map<String, Object> params = new HashMap<>();
        params.put("id", reference.getId());
        params.put("tags", tags.toArray(new String[tags.size()]));

        engine.execute(query, params);
    }

    @Override
    public Set<String> getTags(Reference reference) {
        throw new UnsupportedOperationException("Not supported yet."); //To change body of generated methods, choose Tools | Templates.
    }

    @Override
    public void setPublisherOf(Reference r, Publisher p, int year) {
        final Map<String, Object> params = new HashMap<>();
        params.put("id", r.getId());
        engine.execute("MATCH (:Reference{id: {id}}) -[r:PublishedBy]->() DELETE r", params);

        final String query = "MATCH (r:Reference{id: {id}}) MERGE (p:Publisher{name: {pubId}}) CREATE (r)-[:PublishedBy]->(p)";
        params.put("pubId", p.getName());

        engine.execute(query, params);
    }

    public void setAuthors(Reference r, List<Author> authors) {
        final String query = "MATCH (r:Reference{id: {id}}) "
                + "MERGE (a:Author {id: {authorId}, name: {name}, surname: {surname} }) "
                + "CREATE (r)-[:AuthoredBy{ sortIndex: {sortIndex} }]->(a)";
        final Map<String, Object> params = new HashMap<>();
        params.put("id", r.getId());

        for (int sortIndex = 0; sortIndex < authors.size(); sortIndex++) {
            final Author a = authors.get(sortIndex);
            params.put("authorId", a.getId());
            params.put("name", a.getName());
            params.put("surname", a.getSurname());
            params.put("sortIndex", sortIndex);

            engine.execute(query, params);
        }
    }
    
    public void setAuthor(Reference r, Author author) {
        List<Author> l = new ArrayList<>(1);
        l.add(author);
        setAuthors(r, l);
    }
    

    @Override
    public void save(Publisher publisher) {
        final String query = "CREATE (:Publisher { name: {name} })";
        final Map<String, Object> params = new HashMap<>();
        params.put("name", publisher.getName());
        engine.execute(query, params);
    }

    @Override
    public Set<Publisher> getPublishers() {
        throw new UnsupportedOperationException("Not supported yet."); //To change body of generated methods, choose Tools | Templates.
    }

    private Reference loadFrom(Node a) {
        try (Transaction t = a.getGraphDatabase().beginTx()) {
            Reference r = new Reference((int) a.getProperty("id"),
                    (String) a.getProperty("title"),
                    (String) a.getProperty("abstractText"));
            t.success();
            return r;
        }
    }

    private Author loadAuthorFrom(Node a) {
        try (Transaction t = a.getGraphDatabase().beginTx()) {
            Author au = new Author(
                    (int) a.getProperty("id"),
                    (String) a.getProperty("name"),
                    (String) a.getProperty("surname"));
            t.success();
            return au;
        }
    }

    @Override
    public Set<Reference> findByTag(String tag) {
        throw new UnsupportedOperationException("Not supported yet."); //To change body of generated methods, choose Tools | Templates.
    }

    @Override
    public void close() throws Exception {
        graphDb.shutdown();
    }

    @Override
    public void delete(Author author) {
        String query = "MATCH (a:Author { id: {id}}) OPTIONAL MATCH (a)-[r]-() DELETE a, r";
        Map<String, Object> params = new HashMap<>();
        params.put("id", author.getId());
        engine.execute(query, params);
    }

    public void dumpQuery(String query) {
        System.out.println(query);
        System.out.println(engine.execute(query).dumpToString());
    }

    private class CitedReference extends CitedStats<Reference> {

        private final int citations;
        private final Node reference;

        public CitedReference(int citations, Node reference) {
            this.citations = citations;
            this.reference = reference;
        }

        @Override
        public Reference getEntity() {
            return loadFrom(reference);
        }

        @Override
        public int getCitations() {
            return citations;
        }
    }

    private class CitedAuthor extends CitedStats<Author> {

        private final int citations;
        private final Node reference;

        public CitedAuthor(int citations, Node reference) {
            this.citations = citations;
            this.reference = reference;
        }

        @Override
        public Author getEntity() {
            return loadAuthorFrom(reference);
        }

        @Override
        public int getCitations() {
            return citations;
        }

    }

    @Override
    public Iterator<CitedStats<Reference>> mostCitedReferences() {
        ExecutionResult result
                = engine.execute("MATCH (r:Reference) <-[cit:Cites]- (:Reference) "
                        + "WHERE ANY ( tag IN r.tags WHERE tag = 'TAG 1' ) "
                        + "RETURN r, COUNT(cit) as cnt "
                        + "ORDER BY cnt DESC");

        List<CitedStats<Reference>> ref = new LinkedList<>();
        try (ResourceIterator<Map<String, Object>> ri = result.iterator()) {
            while (ri.hasNext()) {
                Map<String, Object> row = ri.next();
                Node reference = (Node) row.get("r");
                int count = ((Long) row.get("cnt")).intValue();
                CitedStats<Reference> r = new CitedReference(count, reference);
                ref.add(r);
            }
        }
        return ref.iterator();
    }

    @Override
    public Iterator<CitedStats<Author>> mostCitedAuthors() {
        ExecutionResult result
                = engine.execute("MATCH (r:Reference) <-[cit:Cites]- (:Reference), "
                        + "      (r)-[aut:AuthoredBy]->(a:Author) "
                        + "RETURN a, COUNT(cit) as cnt "
                        + "ORDER BY cnt DESC");

        List<CitedStats<Author>> authors = new LinkedList<>();
        try (ResourceIterator<Map<String, Object>> ri = result.iterator()) {
            while (ri.hasNext()) {
                Map<String, Object> row = ri.next();
                Node author = (Node) row.get("a");
                int count = ((Long) row.get("cnt")).intValue();
                CitedStats<Author> cithedAuthor = new CitedAuthor(count, author);
                authors.add(cithedAuthor);
            }
        }
        return authors.iterator();
    }

    @Override
    public void cites(Reference entry, Set<Reference> cited) {
        final String query = "MATCH (r:Reference{id: {idRef}}),\n"
                + " (c:Reference{id: {idCit}})\n"
                + " CREATE (r)-[:Cites]->(c)";

        Map<String, Object> params = new HashMap<>();
        params.put("idRef", entry.getId());

        for (Reference c : cited) {
            params.put("idCit", c.getId());
            engine.execute(query, params);
        }
    }
}
